Viz.js
This project builds Graphviz with
Emscripten and provides a simple
wrapper for using it in the browser.
See Also
Have a look at Dagre, which is not a hack.
Usage
Getting Viz.js
Node.js
N.B.: This library has been fully tested with Node.js 12 LTS and Node.js
13.2+. If you need support for older versions of Node.js (E.G.: 10.x), you
should restrict the use of viz.js
to the synchronous API
only.
import Viz from "@aduh95/viz.js";
import getWorker from "@aduh95/viz.js/worker";
const worker = getWorker();
const viz = new Viz({ worker });
viz
.renderString("digraph{1 -> 2 }")
.then((svgString) => {
console.log(svgString);
})
.catch((error) => {
console.error(error);
})
.finally(
() =>
viz.terminateWorker()
);
If you want to use it from a CommonJS script, you can use the
@aduh95/viz.js/async
wrapper shortcut:
const dot2svg = require("@aduh95/viz.js/async");
dot2svg("digraph{1 -> 2 }")
.then((svgString) => {
console.log(svgString);
})
.catch((error) => {
console.error(error);
});
Note: If you want to your lib to be web-ready, it is recommended to build up
from the first code example rather than the CommonJS one.
Synchronous API
There is a synchronous version of renderString
method available:
const vizRenderStringSync = require("@aduh95/viz.js/sync");
console.log(vizRenderStringSync("digraph{1 -> 2 }"));
Key differences with async API:
- It compiles Graphviz to JavaScript instead of
WebAssembly
, this should come
with a performance hit and a bigger bundled file size (brotli size is 27%
bigger). - It is a CommonJS module, while the rest of the API is written as standard
ECMAScript modules. The upside is this syntax is supported on a wider Node.js
version array.
Note: Using the sync API on the browser main thread is not recommended, it
might degrade the overall user experience of the web page. It is strongly
recommended to use web workers – with the sync or the async API.
Browsers
Using a bundler
You can either use the worker
or the workerURL
on the constructor. Note that
when using workerURL
, Viz
constructor will try to spawn a webworker using
type=module
. If you don't want a module worker, you should provide a worker
instead.
The Worker module exports a function that takes
an Emscripten Module object.
You can use that to tweak the defaults, the only requirement is to define a
locateFile
method that returns the URL of the WASM file.
import initWASM from "@aduh95/viz.js/worker";
import wasmURL from "@aduh95/viz.js/wasm";
initWASM({
locateFile() {
return wasmURL;
},
});
And give feed that module to the main thread:
import Viz from "@aduh95/viz.js";
let viz;
async function dot2svg(dot, options) {
if (viz === undefined) {
viz = new Viz({
worker: new Worker(new URL("./worker.js", import.meta.url), {
type: "module",
}),
});
}
return viz.renderString(dot, options);
}
Using a CDN
If you are using a CDN and don't want a separate file for the worker module,
there is a workaround:
import Viz from "https://unpkg.com/@aduh95/viz.js";
const locateFile = (fileName) =>
"https://unpkg.com/@aduh95/viz.js/dist/" + fileName;
const onmessage = async function (event) {
if (this.messageHandler === undefined) {
const { default: init, onmessage } = await import(
Module.locateFile("render.browser.js")
);
removeEventListener("message", onmessage);
await init(Module);
this.messageHandler = onmessage;
}
return this.messageHandler(event);
};
const vizOptions = {
workerURL: URL.createObjectURL(
new Blob(
[
"const Module = { locateFile:",
locateFile.toString(),
"};",
"onmessage=",
onmessage.toString(),
],
{ type: "application/javascript" }
)
),
};
async function dot2svg(dot, options) {
const viz = new Viz(vizOptions);
return viz.renderString(dot, options);
}
If you want to support browsers that do not support loading webworker as module,
or want a custom message handling, you can use dynamic imports to help you:
function getVizMessageHandler() {
if (this._messageHandler === undefined) {
const vizDistFolder = "https://unpkg.com/@aduh95/viz.js/dist";
const Module = {
locateFile: (fileName) => `${vizDistFolder}/${fileName}`,
};
this._messageHandler = import(Module.locateFile("render.browser.js")).then(
({ default: init, onmessage }) => {
self.removeEventListener("message", onmessage);
return init(Module).then(() => onmessage);
}
);
}
return this._messageHandler;
}
self.addEventListener("message", (event) => {
if (event.data.id) {
getVizMessageHandler()
.then((onmessage) => onmessage(event))
.catch((error) => {
console.error(error);
});
} else {
}
});
Deno
The support is experimental. You would probably need to monkey-patch the
unimplemented web APIs. Please check the test folder for an example of
implementation.
As Deno aims to expose all the web API, you can use the browser implementation.
Building From Source
To build from source, first
install the Emscripten SDK
and Corepack. You'll also need
Node.js 14+ and Deno to run the
tests.
Using Homebrew (macOS or GNU/Linux):
brew install automake bison libtool node pkg-config
Note: Emscripten version number is pinned in the Makefile. If you are willing
to use a different version, you'd need to change the Makefile variable to
match the version you are using.
You will certainly need to tweak config files to make sure your system knows
where it should find each binary.
The build process for Viz.js is split into two parts: building the Graphviz and
Expat dependencies, and building the rendering script files and API.
EMCONFIGURE=emconfigure make deps
emmake make all -j4
emmake make test